package bsh;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bsh.exception.CompilationAssertionFailed;
import bsh.exception.CompilationFailedException;
import bsh.meta.ArrayTypeWrapper;
import bsh.meta.FieldDescriptor;
import bsh.meta.JavaClassWrapper;
import bsh.meta.MethodDescriptor;
import bsh.meta.Scope;
import bsh.meta.TypeWrapper;
import bsh.utils.reflection.BytecodeHelper;

/**
 * @author takeshi
 */
public class PrimarySuffixCompiler implements NodeCompiler {

    private static final int NUM_4 = 4;

    private static final int NUM_3 = 3;

    private final BSHPrimarySuffix node;

    private final CompilationUnit unit;

    /**
     * @param node node
     * @param unit unit
     */
    public PrimarySuffixCompiler(BSHPrimarySuffix node, CompilationUnit unit) {
        super();
        this.unit = unit;
        this.node = node;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.NodeCompiler#compile(org.objectweb.asm.MethodVisitor, bsh.CompilationState)
     */
    public void compile(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        state.addLineNumber(host, this.node.getLineNumber());
        state.assertMinimumStackSize(1);
        int oldStackSize = state.getStackSize();
        switch (this.node.operation) {
        case BSHPrimarySuffix.CLASS:
            this.compileClass(host, state);
            state.assertStackSize(oldStackSize);
            break;

        case BSHPrimarySuffix.INDEX:
            this.compileIndex(host, state);
            state.assertStackSize(oldStackSize);
            break;

        case BSHPrimarySuffix.NAME:
            this.compileName(host, state);
            break;
        default:
            throw new CompilationFailedException(this.node.getLineNumber(), "");
        }
    }

    /**
     * Compila uma expressao de indice
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void compileIndex(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        TypeWrapper stack = state.popStack();
        pushArrayIndex(host, state, this.unit, this.node);
        TypeWrapper popStack = state.popStack();
        if (!popStack.equals(JavaClassWrapper.INT_WRAPPER)) {
            throw new CompilationAssertionFailed("expected: " + JavaClassWrapper.INT_WRAPPER + " but was: " + popStack);
        }
        if (!(stack instanceof ArrayTypeWrapper)) { // not statically proven to be an array
            state.pushStack(stack);
            state.pushStack(popStack);
            AssignmentNodeCompiler.ARRAY_ACCESS_MD.writeInvoke(host, state);

        } else {
            ArrayTypeWrapper concrete = (ArrayTypeWrapper) stack;
            TypeWrapper component = concrete.getComponentType();
            int opcode = component.toASMType().getOpcode(Opcodes.IALOAD);
            host.visitInsn(opcode);
            state.pushStack(component);
        }
    }

    /**
     * Compiles the first child of 'node' as an int to be used as an array index.
     * 
     * @param host the {@link MethodVisitor}
     * @param state the {@link CompilationState}
     * @param unit the {@link CompilationUnit}
     * @param node the {@link BSHPrimarySuffix} that contains the array index
     * @throws CompilationFailedException if something goes bad
     */
    public static void pushArrayIndex(MethodVisitor host, CompilationState state, CompilationUnit unit, BSHPrimarySuffix node)
            throws CompilationFailedException {
        SimpleNode child = node.getChild(0);
        unit.getNodeCompiler(child).compile(host, state);
        TypeWrapper top = state.peekStack();
        unit.getCompilerManager().coerceToType(host, top, JavaClassWrapper.INT_WRAPPER, state);
        top = state.peekStack();
        if (!unit.getCompilerManager().reflectionUtils().isIntPrimitive(top)) {
            throw new CompilationFailedException(node.getLineNumber(), "expected int for array access, but was: " + top);
        }
    }

    /**
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @throws CompilationFailedException e
     */
    private void compileName(MethodVisitor host, CompilationState state) throws CompilationFailedException {
        TypeWrapper targetType = state.peekStack();
        if (this.node.jjtGetNumChildren() == 0) {
            this.compileFieldAccess(host, state, targetType, this.node.field);
        } else {
            this.compileMethodCall(host, state, targetType, this.node.field, (BSHArguments) this.node.getChild(0));
        }
    }

    /**
     * Compila um methodCall
     * 
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param targetType {@link TypeWrapper}
     * @param field String
     * @param child {@link BSHArguments}
     * @throws CompilationFailedException e
     */
    private void compileMethodCall(MethodVisitor host, CompilationState state, TypeWrapper targetType, String field, BSHArguments child)
            throws CompilationFailedException {
        // FIXME can this be a static method?
        TypeWrapper[] args = MethodCallCompiler.resolveParamTypes(child, this.unit, state.currentScope());
        MethodDescriptor method = this.reflectionUtils().findMethodForTypes(targetType, this.node.field, args);
        if (method == null) {
            method = MethodCallCompiler.resolveMethodWithTransformation(targetType, field, args, this.reflectionUtils());
            if (method == null) {
                MethodCallCompiler.logMethodInvocation(unit, node, host, state, child, new MethodCallCompiler.DynamicMethodDescriptor(
                        field, targetType));
                this.compileDynamicInvocation(host, state, field, child);
                return;
            }
        }
        MethodCallCompiler.logMethodInvocation(unit, node, host, state, child, method);
        this.unit.getCompilerManager().coerceToType(host, targetType, method.declaringType(), state, 1);
        if (args.length > 0) {
            MethodCallCompiler.pushArgumentsFor(host, child, method.parameterTypes(), state, this.unit, method.isVarArgs());
        }
        method.writeInvoke(host, state);
    }

    /**
     * @param host MethodVisitor
     * @param state State
     * @param methodName string
     * @param argsNode args
     * @throws CompilationFailedException e
     */
    private void compileDynamicInvocation(MethodVisitor host, CompilationState state, String methodName, BSHArguments argsNode)
            throws CompilationFailedException {
        int id = state.getUnit().thisType().newDynamicInvocationId();
        int oldStackSize = state.getStackSize();
        this.unit.getCompilerManager().coerceToType(host, state.peekStack(), JavaClassWrapper.OBJECT_WRAPPER, state, 1);
        BytecodeHelper.pushIntConst(id, host);
        state.pushStack(int.class);
        host.visitLdcInsn(methodName);
        state.pushStack(String.class);
        final int total = argsNode.jjtGetNumChildren();
        BytecodeHelper.pushIntConst(total, host);
        TypeWrapper targetType = JavaClassWrapper.OBJECT_WRAPPER;
        host.visitTypeInsn(Opcodes.ANEWARRAY, targetType.internalName());
        state.pushStack(new ArrayTypeWrapper(JavaClassWrapper.OBJECT_WRAPPER));
        for (int i = 0; i < total; i++) {
            host.visitInsn(Opcodes.DUP);
            state.pushStack(state.peekStack());
            BytecodeHelper.pushIntConst(i, host);
            state.pushStack(int.class);
            state.getUnit().compileNode(argsNode.getChild(i), host, state);
            state.getUnit().getCompilerManager().coerceToType(host, state.peekStack(), targetType, state);
            int opcode = targetType.toASMType().getOpcode(Opcodes.IASTORE);
            host.visitInsn(opcode);
            state.popStack(NUM_3);
        }
        host.visitVarInsn(Opcodes.ALOAD, 0);
        state.pushStack(state.getUnit().thisType());

        state.assertStackSize(oldStackSize + NUM_4);
        state.addLineNumber(host, argsNode.lastToken.beginLine);
        MethodDescriptor m =
                this.reflectionUtils().findMethodForTypes(
                        JavaClassWrapper.wrapperFor(CompiledScriptHelper.class),
                        "invokeDynamic",
                        new TypeWrapper[]{ JavaClassWrapper.OBJECT_WRAPPER, JavaClassWrapper.INT_WRAPPER,
                                JavaClassWrapper.wrapperFor(String.class), new ArrayTypeWrapper(JavaClassWrapper.OBJECT_WRAPPER),
                                JavaClassWrapper.wrapperFor(CompiledScript.class) });
        m.writeInvoke(host, state);
    }

    /**
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     * @param targetType {@link TypeWrapper}
     * @param field String
     * @throws CompilationFailedException e
     */
    private void compileFieldAccess(MethodVisitor host, CompilationState state, TypeWrapper targetType, String field)
            throws CompilationFailedException {
        FieldDescriptor desc = this.reflectionUtils().findField(targetType, field);
        if (desc == null) {
            DynamicFieldObjectAccessor fieldAcc = new DynamicFieldObjectAccessor(field);
            fieldAcc.loadObject(host, state);
        } else {
            desc.writeGetValueCode(host, state);
        }
    }

    /**
     * @return {@link ReflectionUtils}
     */
    private ReflectionUtils reflectionUtils() {
        return this.unit.getCompilerManager().reflectionUtils();
    }

    /**
     * @param host {@link MethodVisitor}
     * @param state {@link CompilationState}
     */
    private void compileClass(MethodVisitor host, CompilationState state) {
        // special case where the stack size actually have the type we
        // have to push
        TypeWrapper stack = state.popStack();
        host.visitLdcInsn(this.getASMType(stack));
        state.pushStack(Class.class);
    }

    /**
     * @param stack {@link TypeWrapper}
     * @return Type
     */
    private org.objectweb.asm.Type getASMType(TypeWrapper stack) {
        return stack.toASMType();
    }

    /**
     * @param from {@link TypeWrapper}
     * @param scope {@link Scope}
     * @return {@link TypeWrapper}
     * @throws CompilationFailedException e
     */
    public TypeWrapper nodeResultTypeFrom(TypeWrapper from, Scope scope) throws CompilationFailedException {
        switch (this.node.operation) {
        case BSHPrimarySuffix.CLASS:
            return JavaClassWrapper.wrapperFor(Class.class);

        case BSHPrimarySuffix.INDEX:
            if (!(from instanceof ArrayTypeWrapper)) {
                return JavaClassWrapper.OBJECT_WRAPPER;
            }
            return ((ArrayTypeWrapper) from).getComponentType();

        case BSHPrimarySuffix.NAME:
            if (this.node.jjtGetNumChildren() == 0) {
                FieldDescriptor field = this.unit.reflectionUtils().findField(from, this.node.field);
                return field == null ? JavaClassWrapper.OBJECT_WRAPPER : field.fieldType();
            } else {
                // must lookup method...
                BSHArguments child = (BSHArguments) this.node.getChild(0);
                TypeWrapper[] args = MethodCallCompiler.resolveParamTypes(child, this.unit, scope);
                MethodDescriptor method = this.reflectionUtils().findMethodForTypes(from, this.node.field, args);
                if (method == null) {
                    method = MethodCallCompiler.resolveMethodWithTransformation(from, this.node.field, args, this.reflectionUtils());
                    if (method == null) {
                        return JavaClassWrapper.OBJECT_WRAPPER;
                        // throw new CompilationFailedException(node.getLineNumber(), "method not found: " + node.field + " with args: "
                        // + Arrays.toString(args) + " on type: " + from);
                    }
                }
                return method.returnType();
            }
        default:
            throw new CompilationAssertionFailed("cannot get type of PrimarySuffix");
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.NodeCompiler#nodeResultType(bsh.meta.Scope)
     */
    public TypeWrapper nodeResultType(Scope scope) {
        switch (this.node.operation) {
        case BSHPrimarySuffix.CLASS:
            return JavaClassWrapper.wrapperFor(Class.class);
        default:
            throw new CompilationAssertionFailed("cannot get type of PrimarySuffix");
        }
    }

}
